/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.skimmed;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import milk.skimmed.Jarp.Req;

/**
 * Denotes which mods are loaded and in which order.
 * 
 * Manages dependancies, as far as they are known and provides a framework to
 * undo/redo operations.
 * @author Thomas Kerber
 * @version 1.0.0
 */
public class Configuration implements Serializable, Cloneable{
    
    private static final long serialVersionUID = 2029496151266602117L;
    
    /**
     * Denotes the situation a jarp is in in the loading process.
     * @author Thomas Kerber
     * @version 1.0.0
     */
    public static class JarpLoadStatus implements Serializable, Cloneable{
        
        private static final long serialVersionUID = 8726992200834833380L;
        
        /**
         * Whether or not the jarp is to be loaded.
         */
        private boolean loaded = false;
        /**
         * Whether or not the jarps reqs have been met.
         * 
         * MUST be initialized by recheckReqs or initReqs.
         */
        private transient boolean reqsMet = false;
        /**
         * Whether or not the jarps reqs can be met.
         * 
         * MUST be initialized by initReqs.
         */
        private transient boolean canMeetReqs = false;
        /**
         * Whether the jarp can be removed safely without modifying history.
         */
        private boolean isGhost = false;
        
        /**
         * 
         * @return Whether or not the reqs can be met.
         */
        public boolean canMeetReqs(){
            return canMeetReqs;
        }
        
        /**
         * 
         * @return Whether or not the reqs are met.
         */
        public boolean reqsMet(){
            return reqsMet;
        }
        
        /**
         * 
         * @return Whether or not the jarp is loaded.
         */
        public boolean isLoaded(){
            return loaded;
        }
        
        @Override
        public JarpLoadStatus clone(){
            Object o = null;
            try{
                o = super.clone();
            }
            catch(CloneNotSupportedException e){}
            return (JarpLoadStatus)o;
        }
        
    }
    
    /**
     * Denotes a change in the configuration.
     * 
     * Note: This is not cloneable. ConfigDeltas are atomic, there is no need
     * to clone them.
     * @author Thomas Kerber
     * @version 1.0.0
     */
    private interface ConfigDelta extends Serializable{
        /**
         * Reapplies the delta.
         */
        public void redo();
        /**
         * Unapplies the delta.
         */
        public void undo();
        /**
         * 
         * @return If the delta is a "true" delta, i.e. The end of a series of
         *     deltas which all get reapplied/unapplied as one.
         */
        public boolean isCheckpoint();
    }
    
    /**
     * Denotes a jarp being toggled between active/deactive.
     * @author Thomas Kerber
     * @version 1.0.0
     */
    private class SetActive implements ConfigDelta{
        
        private static final long serialVersionUID = 2808434673079369549L;
        
        /**
         * The new value.
         */
        public final boolean setTo;
        /**
         * Which jarp to set.
         */
        public final Jarp jarp;
        /**
         * Whether or not this delta is a checkpoint.
         */
        private final boolean isCheckpoint;
        
        /**
         * Creates.
         * @param j Which jarp to set.
         * @param to What to set to.
         * @param isCheckpoint Whether or not this delta is a checkpoint.
         */
        public SetActive(Jarp j, boolean to, boolean isCheckpoint){
            setTo = to;
            jarp = j;
            // SetActive each of the reqs, if they are not already, without
            // checkpoint.
            if(to){
                for(Req r : j.getReqs()){
                    if(r.req == null)
                        continue;
                    if(!loadStatus.get(loadList.indexOf(r.req)).loaded)
                        addDelta(new SetActive(r.req, true, false));
                }
            }
            // Deactivating DOES NOT deactivate dependent jarps. It's more
            // helpful if the user gets a warning.
            this.isCheckpoint = isCheckpoint;
        }
        
        @Override
        public boolean isCheckpoint(){
            return isCheckpoint;
        }
        
        @Override
        public void redo(){
            getLoadStatus(jarp).loaded = setTo;
            recheckReqs();
        }
        
        @Override
        public void undo(){
            getLoadStatus(jarp).loaded = !setTo;
            recheckReqs();
        }
        
    }
    
    /**
     * Denotes a jarp being moved.
     * @author Thomas Kerber
     * @version 1.0.0
     */
    private class Move implements ConfigDelta{
        
        private static final long serialVersionUID = 7597352570274149071L;
        
        /**
         * The index to move from.
         */
        public final int from;
        /**
         * The index to move to.
         * 
         * This index points to a position in the list while it still *does*
         * contain the jarp at the old position.
         */
        public final int to;
        /**
         * Whether this delta is a checkpoint.
         */
        public final boolean isCheckpoint;
        
        /**
         * 
         * @param j The jarp to move.
         * @param to Where to move it to. The index points to a position in
         *     the list while is still *does* contain the jarp at its old
         *     position.
         * @param isCheckpoint Whether or not this delta is a checkpoint.
         */
        public Move(Jarp j, int to, boolean isCheckpoint){
            from = loadList.indexOf(j);
            this.to = to;
            this.isCheckpoint = isCheckpoint;
        }
        
        @Override
        public boolean isCheckpoint(){
            return isCheckpoint;
        }
        
        @Override
        public void redo(){
            move(from, to);
        }
        
        /**
         * Moves jarp from one position to another.
         * @param from The start position.
         * @param to The end position.
         */
        private void move(int from, int to){
            Jarp j = loadList.get(from);
            from = to > from ? from : from + 1;
            loadList.add(to, j);
            loadList.remove(from);
            JarpLoadStatus jls = loadStatus.get(from);
            loadStatus.add(to, jls);
            loadStatus.remove(from);
            recheckReqs();
        }
        
        @Override
        public void undo(){
            move(to > from ? to - 1 : to, to > from ? from : from - 1);
        }
        
    }
    
    /**
     * The list of jarps in load order (even if they aren't loaded)
     */
    private List<Jarp> loadList = new ArrayList<Jarp>();
    /**
     * The load status of jarps, in the same order as loadList.
     */
    private List<JarpLoadStatus> loadStatus = new ArrayList<JarpLoadStatus>();
    /**
     * The delta history.
     */
    private Stack<ConfigDelta> changeHistory = new Stack<ConfigDelta>();
    /**
     * The delta "future", i.e. redo "history".
     */
    private Stack<ConfigDelta> changeFuture = new Stack<ConfigDelta>();
    /**
     * Whether or not the requirements have been resolved to Jarps yet.
     */
    private transient boolean reqsResolved = false; 
    
    public Configuration(){
        // Does NOT call initReqs as this constructor is used by
        // serializations. (Would lead to uninitialized reqs but them being
        // marked as initialized.)
    }
    
    /**
     * Adds a new delta.
     * @param d The delta.
     */
    private void addDelta(ConfigDelta d){
        changeHistory.add(d);
        if(changeFuture.size() != 0)
            changeFuture = new Stack<ConfigDelta>();
        d.redo();
        ConfigFallbackHistory.INSTANCE.dump();
    }
    
    /**
     * 
     * @return Whether or not it is possible to undo something.
     */
    public boolean canUndo(){
        return changeHistory.size() > 0;
    }
    
    /**
     * 
     * @return Whether or not it is possible to redo something.
     */
    public boolean canRedo(){
        return changeFuture.size() > 0;
    }
    
    /**
     * Undoes the last change.
     */
    public void undo(){
        do{
            ConfigDelta cd = changeHistory.pop();
            changeFuture.push(cd);
            cd.undo();
        }while(!(changeHistory.isEmpty() ||
                changeHistory.peek().isCheckpoint()));
        ConfigFallbackHistory.INSTANCE.dump();
    }
    
    /**
     * Redoes a change.
     */
    public void redo(){
        do{
            ConfigDelta cd = changeFuture.pop();
            changeHistory.push(cd);
            cd.redo();
        }while(!(changeFuture.isEmpty() ||
                changeFuture.peek().isCheckpoint()));
        ConfigFallbackHistory.INSTANCE.dump();
    }
    
    /**
     * 
     * @return All available Jarps. (In load order)
     */
    public List<Jarp> getJarpList(){
        return loadList;
    }
    
    /**
     * 
     * @return The list of loaded jarps, in load order.
     */
    public List<Jarp> getLoadList(){
        List<Jarp> ret = new ArrayList<Jarp>();
        for(int i = 0; i < loadList.size(); i++){
            if(loadStatus.get(i).loaded)
                ret.add(loadList.get(i));
        }
        return ret;
    }
    
    /**
     * Rechecks the requirements. Should be called after most config changes.
     */
    public void recheckReqs(){
        for(int i = 0; i < loadList.size(); i++){
            loadStatus.get(i).reqsMet = loadList.get(i).reqsMet(this);
        }
    }
    
    /**
     * Initializes the requirements. Should be called after a change in
     * available jarps.
     */
    public void initReqs(){
        if(reqsResolved)
            return;
        for(int i = 0; i < loadList.size(); i++){
            for(Req r : loadList.get(i).getReqs()){
                if(!r.assignReq(this))
                    loadStatus.get(i).canMeetReqs = false;
            }
        }
        recheckReqs();
        reqsResolved = true;
    }
    
    /**
     * 
     * @return Whether or not all requirements are met.
     */
    public boolean allReqsMet(){
        initReqs(); // Ensure that reqs have been calculated.
        for(int i = 0; i < loadList.size(); i++)
            if(loadStatus.get(i).loaded)
                if(!loadList.get(i).reqsMet(this))
                    return false;
        return true;
    }
    
    /**
     * 
     * @param j A jarp.
     * @return The laod status associated with this jarp.
     */
    public JarpLoadStatus getLoadStatus(Jarp j){
        initReqs(); // Ensure that reqs have been calculated.
        return loadStatus.get(loadList.indexOf(j));
    }
    
    /**
     * Activates a jarp.
     * @param j The jarp to activate.
     */
    public void setActive(Jarp j){
        setActive(j, true);
    }
    
    /**
     * (De)activates a jarp.
     * @param j The jarp to (de)activate.
     * @param to Whether to activate or deactivate the jarp.
     */
    public void setActive(Jarp j, boolean to){
        addDelta(new SetActive(j, to, true));
    }
    
    /**
     * Moves a jarp to a new position.
     * @param j The jarp.
     * @param to The new position. The index given here is into the list
     *     returned by getJarpList() directly BEFORE this method is called.
     */
    public void move(Jarp j, int to){
        addDelta(new Move(j, to, true));
    }
    
    /**
     * Adds a new jarp.
     * @param j The jarp.
     */
    public void addJarp(Jarp j){
        loadList.add(j);
        loadStatus.add(new JarpLoadStatus());
        setActive(j, true);
        reqsResolved = false;
        initReqs();
        Stack<Configuration> configs =
                ConfigFallbackHistory.INSTANCE.getConfigs();
        for(int i = 0; i < configs.size() - 1; i++){
            Configuration c = configs.get(i);
            c.loadList.add(j.clone());
            JarpLoadStatus jls = new JarpLoadStatus();
            jls.isGhost = true;
            c.loadStatus.add(jls);
            c.reqsResolved = false;
            c.initReqs();
        }
        ConfigFallbackHistory.INSTANCE.dump();
    }
    
    /**
     * Kills all ghost jarps.
     * 
     * Ghost jarps are ones which were added via the fallback history later
     * and actually don't belong in to load list.
     * 
     * This should be called when a config is taken from fallback history
     * to be the main config. Active configs must not contain ghosts.
     */
    void killGhosts(){
        for(JarpLoadStatus jls : loadStatus){
            jls.isGhost = false;
        }
    }
    
    /**
     * Removes a jarp.
     * @param j The jarp to remove.
     */
    public void removeJarp(Jarp j){
        int index = loadList.indexOf(j);
        loadList.remove(index);
        loadStatus.remove(index);
        reqsResolved = false;
        initReqs();
        Stack<Configuration> configs =
                ConfigFallbackHistory.INSTANCE.getConfigs();
        for(int i = 0; i < configs.size() - 1; i++){
            Configuration c = configs.get(i);
            for(int f = 0; i < c.loadList.size(); i++){
                if(c.loadList.get(f).equals(j)){
                    c.loadList.remove(f);
                    c.loadStatus.remove(f);
                    // Ghosts simply get removed. No sweat.
                    if(!c.loadStatus.get(f).isGhost){
                        // If the jarp is loaded... The fallback is removed
                        // entirely.
                        if(c.loadStatus.get(f).loaded){
                            configs.remove(i--);
                        }
                        // Otherwise, its history is erased.
                        else{
                            c.changeFuture = new Stack<ConfigDelta>();
                            c.changeHistory = new Stack<ConfigDelta>();
                        }
                    }
                    break;
                }
            }
        }
        ConfigFallbackHistory.INSTANCE.dump();
    }
    
    @Override
    public Configuration clone(){
        Object o = null;
        try{
            o = super.clone();
        }
        catch(CloneNotSupportedException e){}
        Configuration c = (Configuration)o;
        c.loadList = new ArrayList<Jarp>();
        c.loadStatus = new ArrayList<JarpLoadStatus>();
        for(Jarp j : loadList)
            c.loadList.add(j.clone());
        for(JarpLoadStatus jls : loadStatus)
            c.loadStatus.add(jls.clone());
        return c;
    }
    
}
